<?php  if (!defined('BASEPATH')) exit('No direct script access allowed');

class MJavascript {

	var $jsLibsDir = null; //'assets/js';
	var $jsConstructorDir = null; //'system/application/javascript';
	var $jsCompiledDir = null; //'system/application/javascript_c';
	var $jsConfig = null;
	var $page = null;
	var $constructor = null;
	var $jsFile = null;
	var $libs = array();
	var $ajaxData = array();

	function MJavascript()
	{
		$this->jsLibsDir = $_SERVER['DOCUMENT_ROOT'].DIRECTORY_SEPARATOR.'assets'.DIRECTORY_SEPARATOR.'js';
		$this->jsLibsWebPath = '/assets/js';
		$this->jsConstructorDir =  APPPATH.'javascript';

		log_message('debug', "Javascript Class Initialized");
	}

	function setPage ($page) {
		$this->page = $page;
		$this->constructor = $this->jsConstructorDir.'/constructors/'.$this->convertPage($this->page).'.php';
	}

	/**
	 * collect data about form fields for ajax requests
	 * @param array $data
	 * 		'ref' - name of ajax item
	 * 		'parent' - parent ajax item for current ajax item
	 * 		'code' - js code if in this case ajax handling system doesn't work as needed
	 * 		'trace_by' - Trace element by...
	 * 		'trace_value' - Value for trace element
	 * 		'event' - Event for action
	 * 		'action' - Action when there is an event
	 * 		'params' - additional parameters
	 * 		'lib' - JS library in use.
	 */
	function collect($data = null) {
		static $fields = array();
		static $refs = array();

		if (empty($data)) { return $fields; }

		if (empty($data['trace_by'])) { $data['trace_by'] = false; }
		elseif (!self::checkTrace($data)) { throw new Exception('Unknown javascript trace method'); }

		if (empty($data['event'])) { $data['event'] = false; }
		elseif (!self::checkEvent($data)) { throw new Exception('Unknown javascript event'); }

		if (empty($data['action'])) { $data['action'] = false; }
		elseif (!self::checkAction($data)) { throw new Exception('Unknown javascript action '.$data['action']); }

		$data['trace'] = self::buildTrace($data);
		unset($data['trace_by'], $data['trace_value']);

		if (!empty($data['ref'])) {
			if (!empty($refs[$data['ref']])) {
				$refs[$data['ref']] = self::mergeJsData($refs[$data['ref']], $data); //TODO SOME PROCESS FUNCTION SHOULD BE HERE
			} else {
				$fields[$data['ref']] = $data;
				$refs[$data['ref']] = &$fields[$data['ref']];
			}

			if (!empty($data['parent'])) {
				if (!empty($refs[$data['parent']])) {
					if (empty($refs[$data['parent']]['children'][$data['ref']])) {
						$refs[$data['parent']]['children'][$data['ref']] = $refs[$data['ref']];
						unset($refs[$data['ref']]);
						unset($fields[$data['ref']]);
						$refs[$data['ref']] = &$refs[$data['parent']]['children'][$data['ref']];
					}
				} else {
					$fields[$data['parent']]['children'][$data['ref']] = $refs[$data['ref']];
					$refs[$data['parent']] = &$fields[$data['parent']];
					$refs[$data['ref']] = 0;
					unset($refs[$data['ref']]);
					unset($fields[$data['ref']]);
					$refs[$data['ref']] = &$fields[$data['parent']]['children'][$data['ref']];
				}
			}
		} elseif (!empty($data['parent'])) {
			if (!empty($refs[$data['parent']])) {
				$refs[$data['parent']]['children'][] = $data;
			} else {
				$fields[$data['parent']]['children'][] = $data;
				$refs[$data['parent']] = &$fields[$data['parent']];
			}
		}
	}

	function includeLibs ($lib = null) {
		if (empty($lib)) { return $this->libs; }

		$knownLibs = array('jquery', 'prototype', 'dojo');
		if (!in_array($lib, $knownLibs)) { throw new Exception('Unknown javascript lib'); }
		if (!in_array($lib, $this->libs)) { $this->libs[] = $lib; }
	}

	function mergeJsData ($stack, $new) {
		static $countofChildrenArr = array();
		if (array_key_exists($new['ref'], $countofChildrenArr)) {
			$prevCCount = $countofChildrenArr[$new['ref']];
		} else { $prevCCount = 0; }
		$countofChildren = !empty($stack['children']) ? count($stack['children']) : 0;
		$countofChildrenArr[$new['ref']] = $countofChildren;

		if (!isset($stack['event'])) { $stack['event'] = $new['event']; }
		elseif (is_array($stack['event'])) { $stack['event'][] = $new['event']; }
		else { $stack['event'] = array($stack['event'], $new['event']); }
		unset($new['event']);

		//TODO consider case when one ref has several identical actions
		if (!isset($stack['action'])) {
			$stack['action'] = $new['action'];
			if (isset($new['params'])) { $stack['params'] = $new['params']; }
			else { $stack['params'] = false; }
		} else {
			if ($stack['action'] === false) {
				$stack['action'] = array(0 => false);
				$stack['params'][0] = $stack['params'];
			} elseif (!is_array($stack['action'])) {
				$stack['params'][$stack['action']] = $stack['params'];
				$stack['action'] = array($stack['action'] => $countofChildren);
			} else { //$stack['action'] is array
				end($stack['action']);
				list($key, $val) = each($stack['action']);
				if ($val !== false) { $stack['action'][$key] = $countofChildren - $prevCCount; }
			}
			if ($new['action'] === false) {
				$stack['action'][] = false;
				if (!empty($new['params'])) { $stack['params'][] = $new['params']; }
				else { $stack['params'][] = false; }
			} else {
				$count = 0;
				foreach ($stack['action'] as $key => $value) {
					if (strpos($key, $new['action']) !== false) { $count++; }
				}
				$newActionName = $new['action'].($count ? '_'.$count : '');
				$stack['action'][$newActionName] = 0;
				$stack['params'][$newActionName] = isset($new['params']) ? $new['params'] : false;
			}
		}

		if (empty($stack['trace']) && !empty($new['trace'])) { $stack['trace'] = $new['trace']; }

		return $stack;
	}

	function & getTraces() {
		static $traces = array(
					'jquery' => array(
						'id' => array(
							'prefix' => '$("#',
							'suffix' => '")'),
						'element' => array(
							'prefix' => '$("',
							'suffix' => '")'),
						'class' => array(
							'prefix' => '$(".',
							'suffix' => '")'),
						'selector' => array(
							'prefix' => '$("',
							'suffix' => '")'),
						'name' => array(
							'prefix' => '$("[name=',
							'suffix' => ']")'),
						'xpath' => array(
							'prefix' => '$("',
							'suffix' => '")')));
		return $traces;
	}

	function & getEvents () {
		static $events = array(
			'jquery' => array(
				'collect' => '',
				'blur' => array('code' => '.blur(!action)'),
				'change' => array('code' => '.change(!action)'),
				'click' => array('code' => '.click(!action)'),
				'dblclick' => array('code' => '.dblclick(!action)'),
				'error' => array('code' => '.error(!action)'),
				'focus' => array('code' => '.focus(!action)'),
				'keydown' => array('code' => '.keydown(!action)'),
				'keypress' => array('code' => '.keypress(!action)'),
				'keyup' => array('code' => '.keyup(!action)'),
				'load' => array('code' => '.load(!action)'),
				'mousedown' => array('code' => '.mousedown(!action)'),
				'mousemove' => array('code' => '.mousemove(!action)'),
				'mouseout' => array('code' => '.mouseout(!action)'),
				'mouseover' => array('code' => '.mouseover(!action)'),
				'mouseup' => array('code' => '.mouseup(!action)'),
				'resize' => array('code' => '.resize(!action)'),
				'scroll' => array('code' => '.scroll(!action)'),
				'select' => array('code' => '.select(!action)'),
				'submit' => array('code' => '.submit(!action)'),
				'unload' => array('code' => '.unload(!action)'),
				'timer' => array('code' => '.timer(!action)'),
				'ajaxComplete' => array('code' => 'ajaxComplete(!action)'),
				'ajaxError' => array('code' => 'ajaxError(!action)'),
				'ajaxSend' => array('code' => 'ajaxSend(!action)'),
				'ajaxStart' => array('code' => 'ajaxStart(!action)'),
				'ajaxStop' => array('code' => 'ajaxStop(!action)'),
				'ajaxSuccess' => array('code' => 'ajaxSuccess(!action)'),
			)
		);
		return $events;
	}

	function checkEvent ($data) {
		$events = self::getEvents();

		if (empty($data['lib']))  { $data['lib'] = 'jquery'; }
		self::includeLibs($data['lib']);

		if (!array_key_exists($data['event'], $events[$data['lib']])) {
			return false;
		} else { return true; }
	}

	//TODO consider more complicated cases
	function buildEvent (&$data) {
		$events = self::getEvents();

		if (empty($data['lib']))  { $data['lib'] = 'jquery'; }
		self::includeLibs($data['lib']);

		if (!is_array($data['event'])) { $data['event'] = array($data['event']); }
		foreach ($data['event'] as $event) {
			$data['trace'] .= $event ? $events[$data['lib']][$event]['code'] : '';
		}
		return $data['trace'];
	}

	function & getActions() {
		static $actions = array(
			'jquery' => array(
				'collect' => array(
					'before' => '',
					'code' => '',
					'after' => ''
				),
				'sendData' => array(
					'before' => '',
					'code' => 'function () { $.!method( !url, !children, !callback, !type ); }',
					'after' => ''
			  	),
			  	'getHTML' => array(
					'before' => '',
					'code' => 'getHTML( !url, !children, !callback )',
					'after' => ''
				),
				'getJSON' => array(
					'before' => '',
					'code' => 'getJSON( !url, !children, !callback )',
					'after' => ''
				),
				'val' => array(
					'before' => '',
					'code' => '.val( !data )',
					'after' => ''
				),
				'css' => array(
					'before' => '',
					'code' => '.css( !data )',
					'after' => ''
				),
				'alert' => array(
					'before' => '',
					'code' => 'function () { alert("test"); }',
					'after' => ''
				),
			)
		);
		#collect - get value of element and send to server together with other data (default)
		#sendData - send data to server.
		#getHTML - get some data from server (required some additional parameters)
		#getJSON - get some data from server as JSON string (required some additional parameters)
		return $actions;
	}

	function checkAction ($data) {
		$actions = self::getActions();
		if (empty($data['lib'])) { $data['lib'] = 'jquery'; }
		self::includeLibs($data['lib']);
		if (!array_key_exists($data['action'], $actions[$data['lib']])) {
			return false;
		} else { return true; }
	}

	function buildAction ($data) {
		$actions = self::getActions();

		if (empty($data['lib'])) { $data['lib'] = 'jquery'; }
		self::includeLibs($data['lib']);
		$actions = $actions[$data['lib']];

		if (!is_array($data['action'])) {
			$data['action'] = array($data['action'] => 0);
		}

		$actionCount = count($data['action']);
		$allKidsCount = !empty($data['children']) ? count($data['children']) : 0;
		$busyKidsCount = 0;

		foreach ($data['action'] as $action => $kidCount) {
			if ($counterPos = strpos($action, '_')) { $action = substr($action, 0, $counterPos); }
			if ($kidCount === false) {
				if (substr_count($data['trace'], '!action') > $actionCount--) {
					$data['trace'] = substr_replace($data['trace'], '', strpos($data['trace'], '!action'), 7);
				}
				continue;
			}
			$kidCount = $kidCount ? $kidCount : $allKidsCount - $busyKidsCount;
			$actionData = $actions[$action];

			if (strpos($actionData['code'], '.') === 0) {
				$data['trace'] = $actionData['before'].
								$data['trace'].
								$actionData['code'].
								$actionData['after'];
			} else {
				$data['trace'] = strpos($data['trace'], "!action") !== FALSE
								? $actionData['before'].substr_replace($data['trace'], $actionData['code'], strpos($data['trace'], '!action'), 7).$actionData['after']
								: $actionData['before'].$actionData['code'].$actionData['after'];
			}

			if (isset($data['children']) && strpos($data['trace'], "!children") !== FALSE) {

				$childStr = '';
				$kArr = array_slice($data['children'], $busyKidsCount, $kidCount, true);
				for ($i = 0; $i < count($kArr); $i++) {
					$key = key($kArr);
					$value = $kArr[$key];
					unset($kArr[$key]);
					$kArr[$key] = $value['trace'];
				}
				$childStr = $this->childrenEncode($kArr);
				if (!in_array(array('action' => $action, 'refs' => array_keys($kArr)), $this->ajaxData)) {
					$this->ajaxData[] = array('action' => $action, 'refs' => array_keys($kArr));
				}
				$data['trace'] = str_replace('!children', $childStr, $data['trace']);
			}
			$busyKidsCount += $kidCount;
		}
		return $data['trace'];
	}

	function buildParams ($data) {
		foreach ($data['params'] as $key => $value) {
			if (is_array($value)) {
				foreach ($value as $subkey => $subvalue) {
					if ($keypos = strpos($data['trace'], $subkey)) {
						$data['trace'] = substr_replace($data['trace'], $subvalue, $keypos, strlen($subkey));
					}
				}
			} else {
				if ($keypos = strpos($data['trace'], $key)) {
					$data['trace'] = substr_replace($data['trace'], $value, $keypos, strlen($key));
				}
			}
		}
		return $data['trace'];
	}

	function childrenEncode ($arr) {
		if (is_array($arr)) {
			reset($arr);

			if (count($arr) == 1) {
				return current($arr);
			} else {
				$result = array();
				foreach ($arr as $key => $value) {
					$result[] = (is_string($key) ? '"'.$key.'":': '').$value;
				}
				return '{'.join($result, ', ').'}';
			}
		} else { return '{}'; }
	}


	function checkTrace ($data) {
		$traces = self::getTraces();
		if (empty($data['lib'])) { $data['lib'] = 'jquery'; }
		self::includeLibs($data['lib']);
		if (!array_key_exists($data['trace_by'], $traces[$data['lib']])) {
			return false;
		} else { return true; }
	}

	function buildTrace ($data) {
		if (empty($data['trace_by'])) { return false; }
		$traces = self::getTraces();
		if (empty($data['lib'])) { $data['lib'] = 'jquery'; }
		self::includeLibs($data['lib']);
		return $traces[$data['lib']][$data['trace_by']]['prefix'].
			$data['trace_value'].
			$traces[$data['lib']][$data['trace_by']]['suffix'];
	}

	function getCode () {
		$this->getAjaxData();
		$this->getLibsData();
		if ($this->checkFile() || $this->checkConstructorFile()) {
			if ($this->jsOutOfDate()) {
				$this->constructJs();
			}
			return $this->includeJsLibCode();
		}
	}

	function includeJsLibCode () {
		$scriptTpl = '<script language="javascript" src="%s/%s.js"></script>';
		$libsCode = '';
		foreach ($this->libs as $lib) {
			$libsCode .= sprintf($scriptTpl, $this->jsLibsWebPath, $lib);
		}

		$pageCode = sprintf($scriptTpl, $this->jsLibsWebPath, self::convertPage($this->page));
		return $libsCode.$pageCode;
	}

	function convertPage ($page) {
		return str_replace('/', '!', $page);
	}

	function checkFile () {
		$this->jsFile = $this->jsLibsDir.'/'.$this->convertPage($this->page).'.js';
		if (is_file($this->jsFile)) {
			return true;
		}
		return false;
	}

	function checkConstructorFile () {
		return is_file($this->constructor) && is_readable($this->constructor);
	}

	function jsOutOfDate () {
		//TODO check whether is possible to php constructor to not exist
//		return true;
		$fileStat = stat($this->constructor);
		if ($this->jsFile) {
			$jsFileStat = stat($this->jsFile);
			return $fileStat['mtime'] > $jsFileStat['mtime'];
		} else { return true; }
	}

	function constructJs () {
		include ($this->constructor);
		foreach ($data as $jsItem) {
			$this->collect($jsItem);
		}
		file_put_contents($this->jsFile, $this->process());
	}

	function process () {
		$jsdata = $this->collect();
		$result = '$(document).ready( function () { ';
		foreach ($jsdata as $key => $jsItem) {
			$result .= $this->_process($jsItem)."; \n";
		}
		$result .= ' });';
		$this->saveAjaxData();
		$this->saveLibsData();
		return $result;
	}

	function _process ($item) {
		if (!empty($item['children'])) {
			foreach ($item['children'] as &$child) { $child['trace'] = $this->_process($child); }
		}
		if (!empty($item['event'])) { $item['trace'] = $this->buildEvent($item); }
		if (!empty($item['action'])) { $item['trace'] = $this->buildAction($item); }
		if (!empty($item['params'])) { $item['trace'] = $this->buildParams($item); }
		return $item['trace'];
	}

	function saveAjaxData () {
		return file_put_contents(
				$this->jsConstructorDir.'/ajax/'.$this->convertPage($this->page).'.json', json_encode($this->ajaxData));
	}

	function getAjaxData () {
		if (is_file($this->jsConstructorDir.'/ajax/'.$this->convertPage($this->page).'.json')) {
			$ajaxData = file_get_contents($this->jsConstructorDir.'/ajax/'.$this->convertPage($this->page).'.json');
			$this->ajaxData = json_decode($ajaxData);
			return $this->ajaxData;
		} else {
			return false;
		}
	}

	function saveLibsData () {
		return file_put_contents(
				$this->jsConstructorDir.'/libs/'.$this->convertPage($this->page).'.json', json_encode($this->libs));
	}

	function getLibsData () {
		if (is_file($this->jsConstructorDir.'/libs/'.$this->convertPage($this->page).'.json')) {
			$libs = file_get_contents($this->jsConstructorDir.'/libs/'.$this->convertPage($this->page).'.json');
			$this->libs = json_decode($libs);
			return $this->libs;
		} else {
			return false;
		}
	}
}

?>
